
// tslint:disable-next-line: no-var-requires
require('@root/acme/maintainers').init = () => { }; // 干掉这个无用的方法

import { Service, Application } from 'egg';
import * as ACME from 'acme';
import * as Keypairs from '@root/keypairs';
import * as CSR from '@root/csr';
import * as PEM from '@root/pem';
import * as punycode from 'punycode';
import * as Archiver from 'archiver';
import EC_Key from '../../config/acme.ec.key';

const maintainerEmail = 'admin@zhuziyu.cn';
const subscriberEmail = 'admin@zhuziyu.cn';
const packageAgent = 'YHSD-ACME/1.0.0';

// const directoryUrl = 'https://acme-staging-v02.api.letsencrypt.org/directory';
const directoryUrl = 'https://acme-v02.api.letsencrypt.org/directory';

function challenges(app: Application) {

  return {
    'http-01': {
      async init(_deps) { },
      async zones(_args) { },
      async get(_args) { },
      async set(args) {
        const { challenge: { keyAuthorization, challengeUrl, hostname } } = args;

        const path = challengeUrl.replace(`http://${hostname}`, '');
        app.router.get(path, ctx => { ctx.body = keyAuthorization; });

        app.logger.info('[ACME] on set event: ', path, keyAuthorization);
      },
      async remove(args) {
        const { challenge: { challengeUrl, hostname } } = args;
        const path = challengeUrl.replace(`http://${hostname}`, '');
        let idx = -1;
        for (let i = 0; i < app.router.stack.length; i++) {
          if (app.router.stack[i].path === path) {
            idx = i;
            break;
          }
        }
        if (idx > -1) app.router.stack.splice(idx, 1);
        app.logger.info('[ACME] on remove event: ', idx, path);
      },
    },
  };
}
/**
 * Acme Service
 */
export default class Acme extends Service {

  async getAcme() {
    const acme = ACME.create({
      maintainerEmail,
      packageAgent,
      notify: (ev, msg) => {
        this.logger.info(`[ACME:${ev}]`, JSON.stringify(msg));
      },
    });
    await acme.init(directoryUrl);
    return acme;
  }

  async getAccount() {
    const acme = await this.getAcme();
    console.info('registering new ACME account...');

    const account = await acme.accounts.create({
      subscriberEmail,
      agreeToTerms: true,
      accountKey: EC_Key.private,
    });
    console.info('created account with id', account.key.kid);
    return account;
  }

  async getServerKey() {
    return Keypairs.generate({ kty: 'RSA', format: 'jwk' })
      .then(keypair => keypair.private);
  }

  async generatePEM(domains: string[]) {
    domains = domains.map(i => punycode.toASCII(i));
    const serverKey = await this.getServerKey();
    const csrDer = await CSR.csr({
      jwk: serverKey,
      domains,
      encoding: 'der',
    });
    const csr = PEM.packBlock({
      type: 'CERTIFICATE REQUEST',
      bytes: csrDer,
    });
    console.info('validating domain authorization for ' + domains.join(' '));
    const acme = await this.getAcme();
    const account = await this.getAcme();

    const pems = await acme.certificates.create({
      account,
      accountKey: EC_Key.private,
      csr,
      domains,
      challenges: challenges(this.app),
    });

    const arc = Archiver('zip', { zlib: { level: 9 }, });
    arc.append(pems.cert + '\n' + pems.chain + '\n', { name: 'fullchain.pem' });
    arc.append(pems.chain, { name: 'ca.cer' });
    arc.append(await Keypairs.export({ jwk: serverKey }), { name: 'private.key' });
    arc.finalize();

    this.ctx.set('Content-Disposition', 'attachment;filename=ssl.zip');
    this.ctx.type = 'application/zip, application/octet-stream';

    return arc;
  }
}
